package org.mullin.stratego.client;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;

import org.mullin.stratego.shared.Color;
import org.mullin.stratego.shared.Coord;
import org.mullin.stratego.shared.IllegalMoveException;
import org.mullin.stratego.shared.Match;
import org.mullin.stratego.shared.Move;
import org.mullin.stratego.shared.Piece;
import org.mullin.stratego.shared.RandomState;
import org.mullin.stratego.shared.State;
import org.mullin.stratego.shared.StateChanger;


import com.google.gwt.appengine.channel.client.Channel;
import com.google.gwt.appengine.channel.client.ChannelError;
import com.google.gwt.appengine.channel.client.ChannelFactoryImpl;
import com.google.gwt.appengine.channel.client.SocketListener;
import com.google.gwt.dom.client.AudioElement;
import com.google.gwt.dom.client.MediaElement;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.i18n.client.DateTimeFormat.PredefinedFormat;
import com.google.gwt.media.client.Audio;
import com.google.gwt.user.client.rpc.AsyncCallback;

/**
 * Presenter responsible for handling interactions with the board and updating
 * the state + view.
 * 
 * @author kevin.mullin
 *
 */
public class Presenter {
	
	/**
	 * View to manage visual representation of the state.
	 * 
	 * @author kevin.mullin
	 *
	 */
	public interface View {
		public void setPiece(Coord c, Piece p);
		public void setHidden(Coord c);
		public void setHighlighted(Coord c, boolean highlighted);
		public void setTurn(int turn);
		public void setWinner(int color);
		public void setColor(int color);
		public void animateMove(Move move, Piece piece, boolean attack);

		public void setStatus(String s);
		public void setLogButton(final String s, boolean b);
		public void setMatchList(Collection<String> matchList);

		public void setEmail(String s);
		public void setGameStartDate(String s);
		public void setPlayerRating(String s);
		public void setOpponent(String s);
	}
	
	// Coord representing no piece selected.
	private static final Coord NONE_SELECTED = new Coord(-1, -1);
	
	final View graphics;
	final StateChanger stateChanger;
		
	// Current state of the game.
	State state;
	// Current piece selected by a player, if none selected yet equals NONE_SELECTED
	Coord selected;
	
	private final Audio moveSound = Audio.createIfSupported();
	//private final Audio attackSound = Audio.createIfSupported();
	
	private StrategoServiceAsync strategoService;
	private String channelId;
		
	public int myColor;
	public long matchId;
	public String email;
	
	
	private boolean singlePlayer = false;
	
	private State getInitialState() {
		return new State();
	}
	
	public Presenter(View graphics) {
		this.graphics = graphics;
		stateChanger = new StateChanger();
		state = getInitialState();
		selected = NONE_SELECTED;
		
		
		moveSound.addSource("sounds/move.wav", AudioElement.TYPE_WAV);
		moveSound.addSource("sounds/move.mp3", AudioElement.TYPE_MP3);
		moveSound.setVolume(1.0);
		moveSound.setControls(false);
		moveSound.setPreload(MediaElement.PRELOAD_AUTO);
	}
	
	
	public void login(final String logoutUrl, final String email) {
		graphics.setLogButton(logoutUrl, true);
		graphics.setEmail(email);
	}
	
	public void logout(String email) {
		graphics.setLogButton(email, false);
		graphics.setEmail("");
	}
	
	public void pushMove(Move m) {
		strategoService.sendMove(Move.serialize(m), email, matchId, new AsyncCallback<Void>() {
			
			@Override
			public void onFailure(Throwable caught) {
				
			}
			
			@Override
			public void onSuccess(Void result) {
						
			}
		});
		
	}
	
	public void setStrategoService(StrategoServiceAsync s) {
		strategoService = s;
	}
	
	public void setChannelId(String cid) {
		this.channelId = cid;
	}

	/**
	 * Set current state to provided state. Compares current state with new state,
	 * and for any difference updates view to reflect it.
	 * 
	 * @param s New state.
	 */
	public void setState_NEW(State s) {
		System.out.println("setState NEW, myColor=" + Color.toString(myColor));
		
		for (int i = 0; i < State.ROWS; i++) {
			for (int j = 0; j < State.COLS; j++) {
				Coord c = new Coord(i, j);
				Piece p = s.getPiece(c);
				if (p.color == myColor) {
					p.shown = true;
				}
				graphics.setPiece(c, p);
			}
		}
		
		this.state = State.copyOf(s);
		hideCards();
	}
	
	
	/**
	 * Get current state.
	 * 
	 * @return current state.
	 */
	public State getState() {
		return state;
	}
	
	/**
	 * Can the player make a move?
	 * 
	 * @return true if possible, false otherwise.
	 */
	private boolean canMove() {
		return /*!state.isGameOver() ||*/ state.getTurn() == myColor;
	}

	/**
	 * Flips cards that are not shown or don't belong to the current player.
	 * 
	 */
	public void hideCards() {
		System.out.println("hideCards begin");
		
		for (int i = 0; i < State.ROWS; i++) {
			for (int j = 0; j < State.COLS; j++) {
				Coord c = new Coord(i, j);
				Piece p = state.getPiece(c);
				if (p.shown || p.color == myColor) {
					// current players piece or already revealed
					graphics.setPiece(c, p);
				} else if (!p.equals(State.LAND) && !p.equals(State.WATER) && !p.shown) {
					// unknown enemy piece
					graphics.setHidden(c);
				}
			}
		}
		System.out.println("hideCards end");
	}
	
	/**
	 * Handle a user clicking on the board at coordinate c.
	 * 
	 * @param c Coordinate clicked on.
	 */
	public boolean clickedOn(Coord c, boolean animate) {
		if (!canMove()) {
			// game is already over
			return false;
		}

		if (selected.equals(NONE_SELECTED)) {
			// no piece selected yet
			if (state.isEmptySpot(c) || !(state.getPiece(c).color == state.getTurn())) {
				// clicked on an empty square or an enemy piece
				return false;
			}
			selectPiece(c);
			return false;
		} else if (!state.isEmptySpot(c) && (state.getPiece(c).color == state.getTurn())) {
			// had a piece selected, but clicked on a different one
			unselectPiece();
			selectPiece(c);
			return false;
		} else {
			// had a piece selected, and selected an empty square or an enemy piece
			try {			
				Piece oldSelected = state.getPiece(selected);
				Piece oldC = state.getPiece(c);
				Move move = new Move(c, selected);
				stateChanger.makeMove(state, move);
				Piece newSelected = state.getPiece(selected);
				Piece newC = state.getPiece(c);
				
				if (!animate) {
					if (!oldSelected.equals(newSelected)) {
						// piece at selected changed
						graphics.setPiece(selected, newSelected);
					}
					if (!oldC.equals(newC)) {
						// piece at c changed
						graphics.setPiece(c, newC);
					}
				}
				else {
					graphics.animateMove(move, oldSelected, oldC.equals(State.LAND) ? false : true);
					graphics.setPiece(c, newC);
					graphics.setPiece(selected, newSelected);
				}
				moveSound.pause();
				moveSound.setCurrentTime(0.0);
				moveSound.play();
				
				//send state to server
				if (!singlePlayer) {
					pushMove(move);
				}
				graphics.setTurn(state.getTurn());
				
			} catch (IllegalMoveException e) {
				// TODO: do something smart here
				System.out.println("illegal move in clickedOn");
				return false;
			}
			unselectPiece();
			
			graphics.setTurn(state.getTurn());
			
			if (singlePlayer) {
				System.out.println("MAKING MOVE- SINGLE PLAYER, GETTING MOVE FOR COMPUTER");
				Move computerMove = computer.findMove(state);
				System.out.println("COMPUTER'S MOVE IS " + computerMove);
				handleMove(computerMove);
			}
			
			return true;
		}
	}
	
	/**
	 * Select the piece on the board at coordinate c.
	 * 
	 * @param c Coordinate of piece to select.
	 */
	public void selectPiece(Coord c) {
		selected = c;
		graphics.setHighlighted(c, true);
		// TODO: highlight legal moves for this piece
	}
	
	/**
	 * Forget currently selected piece.
	 * 	
	 */
	public void unselectPiece() {
		graphics.setHighlighted(selected, false);
		selected = NONE_SELECTED;		
		// TODO: after select highlights, clear highlights
	}
	
	
	/**
	 * Handle a move from opponent.
	 * 
	 * @param m Opponent's move.
	 */
	public void handleMove(Move m) {
		try {
			Piece from = state.getPiece(m.from);
			Piece to = state.getPiece(m.to);
			stateChanger.makeMove(state, m);

			graphics.animateMove(m, from, to.equals(State.LAND) ? false : true);
			
			if (state.getPiece(m.to).equals(to)) {
				///System.out.println("setting piece at pos " + to + " to " + to);
				//graphics.setPiece(m.to, to);
			} else {
				System.out.println("setting piece at pos " + to + " to " + from);
				if (from.shown) {
					graphics.setPiece(m.to, from);
				} else {
					graphics.setHidden(m.to);
				}
				graphics.setPiece(m.from, State.LAND);
			}
			
			if (state.getPiece(m.from).equals(from)) {
				//graphics.setPiece(m.from, state.getPiece(m.from));
			} else {
				if (state.getPiece(m.from).shown) {
					graphics.setPiece(m.from, state.getPiece(m.from));
				} else {
					graphics.setPiece(m.from, State.LAND);
				}
				
			}
			
			graphics.setTurn(state.getTurn());
		} catch (IllegalMoveException e) {
			// do something smart here
		}
	}
	
	/**
	 * Handle starting state received from server.
	 * 
	 * @param s Starting state.
	 */
	public void handleStart(State s) {
		setState_NEW(s);
		graphics.setColor(myColor);
		graphics.setTurn(s.getTurn());
	}
	
	public void badMessageRecd() {
		graphics.setStatus("bad message");
	}
	
	public void matched() {
		graphics.setStatus("found opponent.");
	}

	public static final String delim = "&";
	public static final int MSG_TYPE_OFFSET = 0;
	// start message offsets
	public static final int MSG_MATCH_OFFSET = MSG_TYPE_OFFSET + 1;
	public static final int MSG_COLOR_OFFSET = MSG_MATCH_OFFSET  + 1;
	public static final int MSG_STATE_OFFSET = MSG_COLOR_OFFSET + 1;
	// move message offsets
	public static final int MOVE_OFFSET = MSG_TYPE_OFFSET + 1;
	
	// fields identifying the type of a message
	public static final char MOVE = 'M';
	public static final char START = 'S';	
	
	public static final int UNKNOWN_TYPE = -1;
	public static final int START_TYPE = 0;
	public static final int MOVE_TYPE = 1;
		
	/**
	 * Get the substring containing move from message received from server.
	 * 
	 * @param m Message from server.
	 * @return
	 */
	public String parseMove(String m) {
		String r = m.substring(MOVE_OFFSET);
		return r;
	}
	
	/**
	 * Determine type of message received from server.
	 * 
	 * @param m Message from server.
	 * @return
	 */
	public int parseMessage(String m) {
		System.out.println("parseMessage_NEW, c=" + m.charAt(MSG_TYPE_OFFSET) + ", m=" + m);
		
		switch(m.charAt(MSG_TYPE_OFFSET)) {
			case START:
				return START_TYPE;
			case MOVE:
				return MOVE_TYPE;
			default:
				return UNKNOWN_TYPE;
		}
	}
	
	/**
	 * Get list of current matches for player. Populate drop down menu
	 * with list.
	 */
	public void getMatches() {
		strategoService.getMatches(email, new AsyncCallback<Collection<Match>>() {

			@Override
			public void onFailure(Throwable caught) {
				
			}

			@Override
			public void onSuccess(Collection<Match> result) {
				List<String> lst = new ArrayList<String>();
				
				for (Match m : result) {
					StringBuilder sb = new StringBuilder();
					
					if (m.getRedPlayer().equals(email)) {
						sb.append(m.getBluePlayer());
					} else {
						sb.append(m.getRedPlayer());
					}
					sb.append(" ").append(m.getMatchId()).append(" startDate=")
						.append(DateTimeFormat.getFormat(PredefinedFormat.DATE_SHORT).format(m.getStartDate()));
					lst.add(sb.toString());
				}
				
				graphics.setMatchList(lst);
			}
			
		});
	}
	
	/**
	 * Initialize multiplayer. Register with server and set up socket listener.
	 * @param loginInfo
	 */
	public void initMultiPlayer(LoginInfo loginInfo) {
		strategoService.enterGame(loginInfo.getEmailAddress(), new AsyncCallback<Void>() {

			@Override
			public void onFailure(Throwable caught) {
				
			}

			@Override
			public void onSuccess(Void result) {
				getMatches();
			}
			
		});
		
		Channel channel = new ChannelFactoryImpl().createChannel(loginInfo.getToken());
		channel.open(new SocketListener() {

			@Override
			public void onOpen() {
				
			}

			@Override
			public void onMessage(String message) {
				System.out.println("client rec'd message=" + message);
				switch(parseMessage(message)) {
					case MOVE_TYPE:
						handleMove(Move.deserialize(
								parseMove(message))
						);
						
						break;
					case START_TYPE:
						matched();
						
						String[] fields = message.split(delim);
						matchId = Long.parseLong(fields[MSG_MATCH_OFFSET]);
						myColor = fields[MSG_COLOR_OFFSET].equals("R") ? Color.RED : Color.BLUE;
						handleStart(State.deserialize(fields[MSG_STATE_OFFSET]));
						
						String rating = fields[4];
						graphics.setPlayerRating(rating);
						
						graphics.setGameStartDate(DateTimeFormat.getFormat(PredefinedFormat.DATE_SHORT).format(new Date()));
						break;
					case UNKNOWN_TYPE:
						badMessageRecd();
						break;
					default:
						break;
				}
			}

			@Override
			public void onError(ChannelError error) {
				
			}

			@Override
			public void onClose() {
				
			}
			
		});
	}
	
	/**
	 * Auto match against a random player.
	 */
	public void autoMatch() {
		System.out.println("autoMatch: channelId = " + channelId + ", email=" + email);
		strategoService.autoMatch(email, new AsyncCallback<Void>() {

			@Override
			public void onFailure(Throwable caught) {
				
			}

			@Override
			public void onSuccess(Void result) {
				
			}
			
		});

		graphics.setGameStartDate(DateTimeFormat.getFormat(PredefinedFormat.DATE_SHORT).format(new Date()));
	}
	
	/**
	 * Load a match from the server with a given match id.
	 * 
	 * @param s String containing match id + opponents email separated by a space.
	 */
	public void loadMatch(String s) {
		System.out.println("handleMatchLoad- s=" + s);
		
		String[] fields = s.split(" ");
		System.out.println("fields=" + Arrays.asList(fields));
		
		final Long mId = Long.parseLong(fields[1]);
		System.out.println("matchID=" + mId);
		strategoService.getMatch(mId, new AsyncCallback<Match>() {

			@Override
			public void onFailure(Throwable caught) {
				
			}

			@Override
			public void onSuccess(Match result) {
				State newState = State.deserialize(result.getSerializedState());
				
				for (String serializedMove : result.getMoves()) {
					Move m = Move.deserialize(serializedMove);
					
					try {
						stateChanger.makeMove(newState, m);
					} catch (IllegalMoveException e) {
						e.printStackTrace();
						return;
					}
				}
				
				matchId = mId;
				if (result.getRedPlayer().equals(email)) {
					myColor = Color.RED;
					graphics.setOpponent(result.getBluePlayer());
				} else {
					myColor = Color.BLUE;
					graphics.setOpponent(result.getRedPlayer());
				}
				
				handleStart(newState);
				matched();
				graphics.setGameStartDate(DateTimeFormat.getFormat(PredefinedFormat.DATE_SHORT).format(result.getStartDate()));
			}
			
		});
	}
	
	/**
	 * Remove a match from player's history.
	 */
	public void deleteMatch() {
		//if (matchId > 0) {
			strategoService.deleteMatch(matchId, email, new AsyncCallback<Void>() {

				@Override
				public void onFailure(Throwable caught) {
					
				}

				@Override
				public void onSuccess(Void result) {
					getMatches();
					setState_NEW(new State());
				}
				
			});
		//}
	}
	
	
	BadPlayer computer;
	
	public void singlePlayer() {
		RandomState rs = new RandomState();
		State s = rs.generateRandomStartingState();
		
		myColor = Color.RED;
		singlePlayer = true;
		
		handleStart(s);
		computer = new BadPlayer(Color.BLUE, state);
	}
}
